-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local cmn = require 'common'
local mdb = require 'mc.mdb'
local file_sec = require 'utils.file'
local smc = require 'protocol.smc'
local utils = require 'mc.utils'
local utils_core = require 'utils.core'
local client = require 'general_hardware.client'

---@class Unit @组件
---@field private mds_obj table @MDS组件对象
local c_unit = class()

local UNAVAILABLE<const> = 'N/A'
local DEFAULT_INTERFACE<const> = 'bmc.kepler.Systems.Board'
local SELF_TEST_PERIOD <const> = 18000

local function pcbid_to_pcbver(pcbid)
    if pcbid < 1 or pcbid > 26 then -- PcbID的合法取值为1-26
        -- 0为默认值，防止日志刷屏
        if pcbid ~= 0 then
            log:error('pcbid_to_pcbver: The pcbid %s is invalid.', pcbid)
        end
        return nil
    end

    return '.' .. string.char(pcbid + 64) -- 64为字符'A'的前一个字符对应的ASCII码
end

local function logic_version_id_to_version(logic_version_id)
    logic_version_id = logic_version_id & 0xff
    return string.format('%u.%02u', logic_version_id >> 4, logic_version_id & 0xf) -- 高4位和低4位拼装成版本号
end

function c_unit:task_update_logic_version()
    if self:get_prop('LogicVersion') == UNAVAILABLE then
        return
    end
    cmn.skynet.fork(function()
        local timeout = 120
        local version_str = nil
        local ok, version_number
        repeat
            ok, version_number = pcall(function()
                return self:get_prop('LogicVersionID')
            end)
            if ok and version_number ~= 0 then
                version_str = logic_version_id_to_version(version_number)
                self:set_prop('LogicVersion', version_str)
            end
            cmn.skynet.sleep(100)
            timeout = timeout - 1
        until version_str or timeout < 0
        if self.mds_obj then
            log:notice("%s%s get LogicVersionID %s, timeout = %s",
                self.mds_obj.BoardType, self.mds_obj.Slot, version_str, timeout)
        end
    end)
end

function c_unit:task_update_pcb_version()
    if self:get_prop('PcbVersion') == UNAVAILABLE then
        return
    end
    cmn.skynet.fork(function()
        local timeout = 120
        local ok, pcbver_number, pcbver_str
        -- 先赋予PcbVersion默认值.A
        self:set_prop('PcbVersion', '.A')
        repeat
            ok, pcbver_number = pcall(function()
                return self:get_prop('PcbID')
            end)
            if ok and pcbid_to_pcbver(pcbver_number) then
                pcbver_str = pcbid_to_pcbver(pcbver_number)
                self:set_prop('PcbVersion', pcbver_str)
            end
            cmn.skynet.sleep(100)
            timeout = timeout - 1
        until pcbver_str or timeout < 0
        if self.mds_obj then
            log:notice("%s%s update Pcbid %s, timeout = %s",
                self.mds_obj.BoardType, self.mds_obj.Slot, pcbver_str, timeout)
        end
    end)
end

-- 从硬件读取MCU版本并更新本地的资源树属性
function c_unit:task_update_mcu_version()
    if self:get_prop('MCUVersion') == UNAVAILABLE then
        return
    end
    cmn.skynet.fork(function()
        local mcu_version = ''
        local timeout = 120
        repeat
            if self.mcu_obj then
                mcu_version = self.mcu_obj:get_mcu_version()
                self:set_prop('MCUVersion', mcu_version)
            end
            cmn.skynet.sleep(100)
            timeout = timeout - 1
        until mcu_version ~= '' or timeout < 0
    end)
end

local function match_firmware_path(path, position)
    local str = path:match('.*/(.*)')
    local index = string.find(str, '_') -- 查找第一个_的位置
    if not index or string.sub(str, 1, index - 1) ~= 'MCU' then
        return false
    end
    index = path:find('_[^_]*$') -- 查找最后一个_的位置
    return path:sub(index + 1) == position
end

-- 监听对应firmware上version属性的变化
function c_unit:listen_firmware_version_changed()
    client:OnFirmwareInfoPropertiesChanged(function(values, path, _)
        log:info('===== general_hardware listen firmware version %s =======', path)
        if match_firmware_path(path, self.position) then
            if not values['Version'] then
                return
            end
            self:set_prop('MCUVersion', values['Version']:value())
        end
    end)
end

function c_unit:has_mcu()
    -- 判断RefMCUChip在SR中是否是对象引用
    if self:get_prop('RefMCUChip') then
        local ok, read_method = pcall(function()
            return self:get_prop('RefMCUChip').Read
        end)
        if ok and read_method ~= nil then
            return true
        end
    end
    return false
end

function c_unit:has_smc()
    -- 判断RefMCUChip在SR中是否是对象引用
    if self:get_prop('RefSMCChip') then
        local ok, read_method = pcall(function()
            return self:get_prop('RefSMCChip').Read
        end)
        if ok and read_method ~= nil then
            return true
        end
    end
    return false
end

function c_unit:set_ref_mcu_obj(obj)
    self.mcu_obj = obj
end

function c_unit:update_cpld_status(cpld_test_num)
    local cpld_status = 0
    for i = 1, cpld_test_num do
        if self.fail_count_list[i] >= 12 then
            -- 对应位设置为1,用于告警
            cpld_status = cpld_status | (1 << (i - 1))
        end
    end

    if cpld_status == self:get_prop('CpldStatus') then
        return
    end

    self:set_prop('CpldStatus', cpld_status)
end

function c_unit:cpld_self_test_process(cpld_test_num, val_read)
    local val_read_list = {}
    local val_write_list = {}
    local val_write = 0
    local offset
    for i = 1, cpld_test_num do
        offset = 8 * (i - 1)
        val_read_list[i] = (val_read >> offset) & 0xff
        -- 与硬件规定的自检逻辑,通过来回写入0x55和0xaa来确定通信是否正常
        val_write_list[i] = val_read_list[i] == 0x55 and 0xaa or 0x55
        val_write = val_write + (val_write_list[i] << offset)
    end
    local ok = pcall(function()
        self:set_prop('CpldTestReg', val_write)
    end)
    if not ok then
        log:info('[Unit] set CpldTestReg object fail, error: %s %s', self.mds_obj.name, val_write)
        val_write = -1
    end
    cmn.skynet.sleep(50) -- 延时500ms防止写入没有生效
    local check_value
    ok, check_value = pcall(function()
        return self:get_prop('CpldTestReg')
    end)
    if not ok or check_value == nil then
        log:info('[Unit] get CpldTestReg object fail, error: %s %s', self.mds_obj.name, check_value)
        check_value = -1
    end

    if check_value < 0 or val_write < 0 then
        for i = 1, cpld_test_num do
            -- 通讯异常失败次数上限3次,3min
            self.fail_count_list[i] = self.fail_count_list[i] and self.fail_count_list[i] + 4 or 4
        end
        self:update_cpld_status(cpld_test_num)
        return
    end
    local temp_check_value
    for i = 1, cpld_test_num do
        offset = 8 * (i - 1)
        temp_check_value = (check_value >> offset) & 0xff
        if temp_check_value == val_write_list[i] then
            self.fail_count_list[i] = 0
        else
            -- cpld内部错误次数上限3次,15s
            self.fail_count_list[i] = self.fail_count_list[i] and self.fail_count_list[i] + 4 or 4
        end
    end
    self:update_cpld_status(cpld_test_num)
end

function c_unit:cpld_self_test()
    -- 某些cpld不需要自检 或者 cpld处于热生效状态，不进行自检
    if self.cpld_validating then
        return
    end
    -- 未关联自检寄存器
    local ok, val_read = pcall(function()
        return self:get_prop('CpldTestReg')
    end)
    if not ok or val_read == nil then
        return
    end
    local cpld_test_num = self:get_prop('CpldTestNum') or 1
    self:cpld_self_test_process(cpld_test_num, val_read)
end

-- CPLD告警状态更新任务, 根据Cpld测试寄存器的情况进行设置, 默认配置成0, 表示没有告警
function c_unit:task_update_cpld_status()
    cmn.skynet.fork(function()
        log:info('[Unit] start task (CpldStatus update) for %s', self.mds_obj.name)
        cmn.skynet.sleep(SELF_TEST_PERIOD)
        while true do
            cmn.skynet.sleep(6000)
            self:cpld_self_test()
        end
    end)
end

-- CPLD/MCU运行状态更新任务, 1 - 正常， 0 - 异常
function c_unit:task_update_running_status()
    cmn.skynet.fork(function()
        log:info('[Unit] start task (RunningStatus update) for %s', self.mds_obj.name)
        local timeout = 60
        local status, version
        local ok, loigic_ver_number
        while true do
            status = 1
            -- 关联到MCU对象并且通信失败
            if self:has_mcu() and self.mcu_obj then
                version = self.mcu_obj:get_mcu_version()
                if not version then
                    status = 0
                end
            end

            -- 电源板通过MCU实现SMC协议，存在SMC但是没有逻辑版本号
            if self:get_prop('LogicVersion') ~= UNAVAILABLE and self:has_smc() then
                ok, loigic_ver_number = pcall(function()
                    return self:get_prop('LogicVersionID')
                end)

                if not ok or loigic_ver_number == 0 then
                    status = 0
                end
            end
            -- RunningStatus在SR文件中为固定值或未配置
            self:set_prop('RunningStatus', status)
            timeout = timeout - 1
            if timeout < 0 then
                break
            end
            cmn.skynet.sleep(200)
        end
    end)
end

-- 获取Container对象，默认为Component对象
function c_unit:get_container_object()
    require 'general_hardware.json_types.Component'
    local COMPONENT_PRE_PATH = '/bmc/kepler/Systems/1/Components/'
    local COMPONENT_INTERFACE = 'bmc.kepler.Systems.Component'

    -- Container在SR中是通过Connector传递的对象名称
    local container_obj_name = self:get_prop('Container')
    if not container_obj_name or container_obj_name == '' then
        return false
    end

    local ok, rsp = pcall(mdb.get_object, self.bus, COMPONENT_PRE_PATH .. container_obj_name,
        COMPONENT_INTERFACE)
    if not ok then
        log:info('[Unit] get container object fail, error: %s', rsp)
        return false
    end
    return rsp
end

-- 框架支持从上一级传递Slot之后需要删除
function c_unit:update_slot()
    -- DeviceName在SR文件中为固定值或未配置
    local device_name = self:get_prop('DeviceName')
    if not device_name or #device_name <= 1 then
        return
    end
    local slot = tonumber(device_name:match('(%d+)'))
    if slot then
        -- Slot在SR文件中为固定值或未配置
        self:set_prop('Slot', slot)
    end
end

function c_unit:get_prop(name, interface)
    if not self.mds_obj then
        return nil
    end

    if interface then
        return self.mds_obj[interface][name]
    elseif self.mds_obj[name] then
        return self.mds_obj[name]
    else
        return self.mds_obj[DEFAULT_INTERFACE][name]
    end
end

function c_unit:set_prop(name, val, interface)
    if not self.mds_obj then
        return nil
    end

    if interface then
        self.mds_obj[interface][name] = val
    elseif self.mds_obj[name] then
        self.mds_obj[name] = val
    else
        self.mds_obj[DEFAULT_INTERFACE][name] = val
    end
end

function c_unit:set_upgarde_flag(val)
    self:set_prop('CurrentUpgradeStatus', val)
end

function c_unit:set_fpga_status(val)
    self:set_prop('FpgaStatus', val)
end

function c_unit:get_system_id()
    return self.system_id
end

function c_unit:get_mds_obj()
    return self.mds_obj
end

function c_unit:destroy()
end

function c_unit:ctor(bus, mds_obj, position, db)
    self.bus = bus
    self.db = db
    self.mds_obj = mds_obj
    self.position = position
    self.cpld_validating = false
    self.fail_count_list = {}
    self.system_id = 1
    local ok, sys_id = pcall(mds_obj.get_system_id, mds_obj)
    if not ok then
        log:error("get_system_id failed, err: %s", sys_id)
        return
    end
    self.system_id = sys_id
end

function c_unit:init()
    self:task_update_logic_version()
    self:task_update_pcb_version()
    self:task_update_running_status()
    self:task_update_cpld_status()
    self:listen_firmware_version_changed()
    self:update_slot()
end

local SMC_GET_SOFT_VERSION_CMD<const> = 0x900
local SMC_GET_SOFT_VERSION_RSP_LEN<const> = 4
local SMC_GET_DFX_INFO_CMD<const> = 0x1D00

function c_unit:write_dfx_info(file_name, dfx_info, dfx_len)
    local file, err = file_sec.open_s(file_name, 'w+')
    if not file then
        log:error('open %s failed, err: %s', file_name, err)
        return
    end
    utils.safe_close_file(file, function()
        utils_core.chmod_s(file_name, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
        local buff = 'offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n'
        buff = buff .. '------------------------------------------------------\n'
        local i = 1
        local num = 0
        while i <= dfx_len do
            if (i - 1) % 16 == 0 then
                buff = buff .. string.format('%.4x: ', i - 1)
            end
            num = string.sub(dfx_info, i, i):byte()
            buff = buff .. string.format(' %.2x', num)
            i = i + 1
            if (i - 1) % 16 == 0 then
                buff = buff .. '\n'
            end
        end
        file:write(buff)
    end)
end

function c_unit:get_dfx_info()
    if not self:has_smc() then
        log:error('smc is null')
        return nil
    end
    local chip = self:get_prop('RefSMCChip')
    local ok, rsp = pcall(smc.chip_blkread, chip, SMC_GET_SOFT_VERSION_CMD,
        SMC_GET_SOFT_VERSION_RSP_LEN)
    if not ok or not rsp then
        log:error('SMC read (get software version) fail')
        return nil
    end
    -- 获取软件信息第三位为dfx长度
    local dfx_len = string.sub(rsp, 3, 3):byte()
    if dfx_len == 0 then
        -- smc拿到dfx长度为0的话取默认最大长度255
        dfx_len = 255
    end
    ok, rsp = pcall(smc.chip_blkread, chip, SMC_GET_DFX_INFO_CMD, dfx_len)
    if not ok or not rsp then
        log:error('SMC read (get dfx info) fail')
        return nil
    end
    return rsp, dfx_len
end

function c_unit:on_dump_cb(dest_path)
    local device_name = self:get_prop('DeviceName')
    if not device_name then
        log:error('[unit] device_name get fail')
        return
    end
    local file_name = 'DFX_' .. device_name
    local dest_file = dest_path .. '/' .. file_name
    local dfx_info, dfx_len = self:get_dfx_info()
    if not dfx_info then
        log:error('%s get_dfx_info fail', device_name)
        return
    end
    if dfx_len == 0 then
        log:error('%s dfx info is 0', device_name)
        return
    end
    self:write_dfx_info(dest_file, dfx_info, dfx_len)
    log:notice('%s collect finish', device_name)
end

c_unit.pcbid_to_pcbver = pcbid_to_pcbver
c_unit.logic_version_id_to_version = logic_version_id_to_version
return c_unit
